"""
定时任务
封装定时任务装饰器@Scheduler
封装支持持久化的定时任务添加方法Scheduler_add
定时任务持久化默认存储在data.db的apscheduler_jobs表中

对外开放管理类SchedulerManager
孪生数据存放在data.db的jobinfo表中
通过在网页修改该表的内容，实现对定时任务的管理。
"""

import threading
from db import Sqlite
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from common import *
import traceback
from functools import wraps
import json


class Trigger():
    class cron():
        """ cron定时器触发器类 """
        def __init__(self, ss='*', mm='*', hh='*', dd='*', MM='*', ww='*', yy='*', day_of_week='0-6') -> None:
            """
            默认每秒执行一次 运行逻辑如下:
            :value
                :* : 表示每次都执行
                n : 表示值等于n时执行
                n,m : 表示值等于n或者m时执行
                n-m : 表示值在n到m之间每次都会执行
                n/m : 表示以m分割，0-m之间，值等于n时执行
            """
            self.second = ss
            self.minute = mm
            self.hour = hh
            self.day = dd
            self.month = MM
            self.week = ww
            self.year = yy
            self.day_of_week = day_of_week

    class interval():
        """ interval定时器触发器类 """
        def __init__(self, seconds=0, minutes=0, hours=0, days=0, weeks=0, start_date=None, end_date=None) -> None:
            """
            间隔指定时间执行
            :param seconds: 秒
            :param minutes: 分
            :param hours: 小时
            :param days: 天
            :param weeks: 周
            :param months: 月
            """
            self.seconds = seconds
            self.minutes = minutes
            self.hours = hours
            self.days = days
            self.weeks = weeks
            self.start_date = start_date
            self.end_date = end_date

    class date():
        """ date定时器触发器类 """
        def __init__(self, date_time) -> None:
            """
            指定时间执行
            :param date_time: 指定时间
            """
            self.date_time = date_time


class JOBINFO():
    """ 定时任务的附加信息 """
    def __init__(self,
                 name,
                 description,
                 task,
                 args,
                 kwargs,
                 trigger=None,
                 trigger_type=None,
                 trigger_args=None,
                 is_active=1,
                 create_time=None,
                 update_time=None,
                 stack_info=None,
                 **ignore) -> None:
        """
        @param:
            name: 任务名称,唯一，与Jobs里面的id关联
            description: 任务描述
            task: 任务执行的方法
            args: 任务执行的参数
            kwargs: 任务执行的参数
            trigger: 任务触发器对象，如果传了trigger，则trigger_type和trigger_args无效
            trigger_type: 触发器类型
            trigger_args: 触发器参数
            is_active: 任务是否激活
            create_time: 任务创建时间
            stack_info: 任务创建时的堆栈信息
        """
        self.name = name
        self.description = description
        self.task = task
        self.args = tuple(args.split(',') if args else [])
        self.kwargs = kwargs
        if trigger:
            self.trigger_type = trigger.__class__.__name__
            self.trigger_args = trigger.__dict__
        else:
            # self.trigger = getattr(Trigger, trigger_type)(trigger_args)
            self.trigger_type = trigger_type
            self.trigger_args = json.loads(trigger_args.replace("'", '"').replace('None', 'null'))
        self.is_active = is_active
        self.create_time = create_time or datetime.now()
        self.update_time = update_time or datetime.now()
        self.stack_info = stack_info or ''


class JOB_CONTENT():
    """ 作业库 """
    lock = threading.Lock()

    @staticmethod
    def test_scheduler(arg1, arg2):
        JOB_CONTENT.lock.acquire()
        print('threadid:{}->{}-{}:{}'.format(threading.get_ident(), arg1, arg2, datetime.now()))
        JOB_CONTENT.lock.release()


class SchedulerManager():
    """ 作业任务管理，管理所有的定时任务 """

    JOB_DEFAULTS_CONFIG = {
        'coalesce': True,  # 过时任务是否忽略
        'replace_existing': True,  # 如果存在相同id任务，是否替换
        'max_instances': 10,  # 最大实例数
        # 'misfire_grace_time': 60  # 容错时间
    }
    # 独立定时任务管理
    __detached_scheduler_dict = {}

    # region 独立定时任务管理
    @staticmethod
    def Scheduler(trigger, job_id=None, max_instances=1):
        """
        独立定时任务装饰器。
        @param:
            trigger: 触发器对象，需要是Trigger的子类（cron, interval, date）
            max_instances: 最大实例数，默认为1，如果任务线程阻塞，将在下次执行。大于1时，将会安排其他线程执行
        @return:
            job_id: 作业id
        """
        def deractor(func):
            @wraps(func)
            def add_into_scheduler(*args, **kwargs):
                """
                @param:
                    func: 被装饰的函数
                    args: 函数参数
                    kwargs: 函数参数
                """
                return SchedulerManager.add_job(func, args, kwargs, trigger, job_id, max_instances)

            return add_into_scheduler

        return deractor

    @staticmethod
    def add_job(func, args=None, kwargs=None, trigger=None, job_id=None, max_instances=1):
        """
        添加一个独立的定时任务。
        @param:
            func: 定时任务函数
            trigger: 触发器对象，需要是Trigger的子类（cron, interval, date）
            job_id: 定时任务id，相同id的会被覆盖，如果不指定，则使用函数名+参数
            max_instances: 最大实例数，默认为1，如果任务线程阻塞，将在下次执行。大于1时，将会安排其他线程执行
            args: 函数参数
            kwargs: 函数参数
        @return:
            job_id: 定时任务id
        """
        try:
            assert trigger, '触发器不能为空'

            _detached_job = BackgroundScheduler(timezone='Asia/Shanghai')

            SchedulerManager.JOB_DEFAULTS_CONFIG['max_instances'] = max_instances
            job_id = job_id or func.__name__ + str(args) + str(kwargs)
            _detached_job.add_job(func,
                                  trigger.__class__.__name__,
                                  **trigger.__dict__,
                                  **SchedulerManager.JOB_DEFAULTS_CONFIG,
                                  id=job_id,
                                  args=args,
                                  kwargs=kwargs)
            _detached_job.start()
            SchedulerManager.__detached_scheduler_dict[job_id] = _detached_job
            print('任务 <{}> 开始运行'.format(job_id))
            return job_id
        except Exception as e:
            print(e)
            print(traceback.format_exc())
            return None

    @staticmethod
    def pause_job(job_id):
        """ 停止指定独立任务 """
        try:
            if job_id in list(SchedulerManager.__detached_scheduler_dict.keys()):
                SchedulerManager.__detached_scheduler_dict[job_id].pause_job(job_id)
                print('任务 <{}> 已停止'.format(job_id))
        except Exception as e:
            print(e)
            print(traceback.format_exc())

    @staticmethod
    def resume_job(job_id):
        """ 恢复指定独立任务 """
        try:
            if job_id in SchedulerManager.__detached_scheduler_dict:
                SchedulerManager.__detached_scheduler_dict[job_id].resume_job(job_id)
                print('任务 <{}> 已恢复'.format(job_id))
        except Exception as e:
            print(e)
            print(traceback.format_exc())

    @staticmethod
    def remove_job(job_id):
        """ 删除指定独立任务 """
        try:
            if job_id in SchedulerManager.__detached_scheduler_dict:
                SchedulerManager.__detached_scheduler_dict[job_id].remove_job(job_id)
                del SchedulerManager.__detached_scheduler_dict[job_id]
                print('任务 <{}> 已删除'.format(job_id))
        except Exception as e:
            print(e)
            print(traceback.format_exc())

    # endregion


class Persistence_Scheduler_Manager():
    """ 持久化定时任务管理 """

    # 存储配置
    SCHEDULER_JOBSTORES = {
        'default': SQLAlchemyJobStore(url='sqlite:///data.db')
        # 'type': 'sqlalchemy',
        # 'url': 'sqlite:///data.db',
        # 'tablename': 'jobs'
    }
    # 线程池配置
    SCHEDULER_EXECUTOR = {'default': ThreadPoolExecutor(10), 'processpool': ProcessPoolExecutor(10)}
    # 作业默认配置
    JOB_DEFAULTS_CONFIG = {
        'coalesce': True,  # 过时任务是否忽略
        'replace_existing': True,  # 如果存在相同id任务，是否替换
        'max_instances': 10,  # 最大实例数
        # 'misfire_grace_time': 60  # 容错时间
    }

    __jobs = None  # 存储所有定时任务
    __jobs_info = None  # 存储所有定时任务信息
    # endregion

    # 持久化定时任务关键属性,这些属性被修改，需要更新定时任务
    __presistence_job_important_attr = ['task', 'args', 'kwargs', 'trigger_type', 'trigger_args', 'is_active']

    def __init__(self):
        """ 使用类变量管理定时任务和任务信息，防止重复实例化带来的问题 """
        from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED
        if self.__class__.__jobs is None:
            self.__class__.__jobs = BackgroundScheduler(timezone='Asia/Shanghai',
                                                        jobstores=self.__class__.SCHEDULER_JOBSTORES,
                                                        executors=self.__class__.SCHEDULER_EXECUTOR)
            self.__class__.__jobs.start()
            self.__class__.__jobs.add_listener(self.__job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
            # 初始化任务信息
            Sqlite().execute('\
                create table if not exists jobinfo(\
                    name varcher(64) primary key not null,\
                    description varcher(128),\
                    task varchar(64),\
                    args varchar(256),\
                    kwargs varchar(256),\
                    trigger_type varchar(16),\
                    trigger_args varchar(128),\
                    trigger_args_simple varchar(128),\
                    is_active int,\
                    create_time datetime,\
                    update_time datetime,\
                    stack_info varchar(1024)\
                )\
                ')
            self.__class__.__jobs_info = [JOBINFO(**job_info_dict) for job_info_dict in Sqlite().select_to_dict('select * from jobinfo')]
            self.init_presistence_job()

    def __job_listener(self, event):
        """
        任务监听器。
        @param:
            event: 任务事件
        """
        if event.exception:
            print('任务出现异常：{}'.format(event.job_id))
            print(event.exception)
            print(traceback.format_exc())
        else:
            print('任务执行完毕：{}'.format(event.job_id))

    # region 持久化任务管理

    def init_presistence_job(self):
        """ 初始化持久化任务 """
        for jobinfo in self.__class__.__jobs_info:
            if not self.__class__.__jobs.get_job(jobinfo.name):
                print('初始化任务 <{}> 启动'.format(jobinfo.name))
                print(jobinfo.args, jobinfo.kwargs)
                try:
                    self.__class__.__jobs.add_job(getattr(JOB_CONTENT, jobinfo.task),
                                                  jobinfo.trigger_type,
                                                  **jobinfo.trigger_args,
                                                  **self.JOB_DEFAULTS_CONFIG,
                                                  id=jobinfo.name,
                                                  args=jobinfo.args,
                                                  kwargs=convert_equlexp_to_dict(jobinfo.kwargs))
                except Exception as e:
                    print('初始化任务 <{}> 失败'.format(jobinfo.name))
                    print(e)
            else:
                if not jobinfo.is_active:
                    print('初始化任务 <{}> 停止'.format(jobinfo.name))
                    self.__class__.__jobs.pause_job(jobinfo.name)

        for job in self.__class__.__jobs.get_jobs():
            if job.id not in [jobinfo.name for jobinfo in self.__class__.__jobs_info]:
                print('初始化任务 <{}> 删除'.format(job.id))
                self.__class__.__jobs.remove_job(job.id)

    def add_persistence_job(self, jobinfo: JOBINFO):
        """
        添加一个持久化任务。
        @param:
            jobinfo: 任务信息对象
        """
        try:
            if jobinfo.name in [_jobinfo.name for _jobinfo in self.__class__.__jobs_info]:
                self.update_persistence_job(jobinfo)
            else:
                self.__class__.__jobs_info.append(jobinfo)
                self.__class__.__jobs.add_job(getattr(JOB_CONTENT, jobinfo.task),
                                              jobinfo.trigger_type,
                                              **jobinfo.trigger_args,
                                              **self.JOB_DEFAULTS_CONFIG,
                                              args=jobinfo.args,
                                              kwargs=convert_equlexp_to_dict(jobinfo.kwargs),
                                              id=jobinfo.name)
                # print(jobinfo.trigger_args)
                print('添加持久化任务：{}'.format(jobinfo.name))
                # Sqlite().insert_object_list([jobinfo])
        except Exception as e:
            print(e)
            print(traceback.format_exc())

    def update_persistence_job(self, jobinfo: JOBINFO):
        """
        更新一个持久化任务。
        @param:
            jobinfo: 任务信息对象
        """
        try:
            for jobinfo_index, _jobinfo in enumerate(self.__class__.__jobs_info):
                if _jobinfo.name == jobinfo.name and jobinfo.__dict__ != _jobinfo.__dict__:  # 如果任务名称相同，且任务信息不同，则更新
                    self.__class__.__jobs_info[jobinfo_index] = jobinfo
                    print('更新持久化任务：', jobinfo.name)
                    # 判断关键信息是否被修改
                    update_attr = [attr for attr in compare_object(jobinfo, _jobinfo) if attr in self.__class__.__presistence_job_important_attr]
                    if update_attr != []:  # 如果关键字段被修改，则更新任务
                        if update_attr == ['is_active']:  # 如果只有任务状态被修改，根据状态值停止/启动任务
                            if jobinfo.is_active:
                                self.__class__.__jobs.resume_job(jobinfo.name)
                            else:
                                self.__class__.__jobs.pause_job(jobinfo.name)
                        elif jobinfo.is_active == 1:  # 如果触发器或者调用参数被修改，并且任务处于启动状态，则重新布置任务
                            self.__class__.__jobs.remove_job(jobinfo.name)
                            print(jobinfo.args, jobinfo.kwargs)
                            self.__class__.__jobs.add_job(getattr(JOB_CONTENT, jobinfo.task),
                                                          jobinfo.trigger_type,
                                                          **jobinfo.trigger_args,
                                                          **self.JOB_DEFAULTS_CONFIG,
                                                          args=jobinfo.args,
                                                          kwargs=convert_equlexp_to_dict(jobinfo.kwargs),
                                                          id=jobinfo.name)
                    # Sqlite().update_object_list([jobinfo])
                    break
        except Exception as e:
            print(e)
            print(traceback.format_exc())

    def remove_persistence_job(self, name):
        """
        删除一个持久化任务。
        @param:
            name: 任务名称
        """
        try:
            _jobinfo = self.get_persistence_jobinfo(name)
            if _jobinfo:
                self.__class__.__jobs_info.remove(_jobinfo)
                self.__class__.__jobs.remove_job(name)
                print('删除持久化任务：{}'.format(name))
        except Exception as e:
            print(e)
            print(traceback.format_exc())

    def get_persistence_jobinfo(self, name):
        """
        获取指定名称的持久化任务。
        @param:
            name: 任务名称
        """
        jobs = [_jobinfo for _jobinfo in self.__class__.__jobs_info if _jobinfo.name == name]
        return None if len(jobs) == 0 else jobs[0]

    def refresh_presistence_job(self, name=None):
        if not name:
            # operate = {}
            jobs_new_info = [JOBINFO(**info) for info in Sqlite().select_to_dict('select * from jobinfo')]
            add, remove, upd = compare_object_list_by_attr(jobs_new_info, self.__class__.__jobs_info, 'name')
            for _jobinfo in add:
                self.add_persistence_job(_jobinfo)
            for _jobinfo in upd:
                self.update_persistence_job(_jobinfo)
            for _jobinfo in remove:
                self.remove_persistence_job(_jobinfo.name)

    # endregion


if __name__ == '__main__':

    @SchedulerManager.Scheduler(Trigger.interval(seconds=3))
    def test_job(a, b):
        print('{}:{}->{}'.format(a, b, datetime.now()))

    # test_scheduler('arg1', 'arg2')
    # test_scheduler('arg3', 'arg4')
    # globals()['test_scheduler']()
    # SchedulerManager.add_job(globals()['test_scheduler'], args=('arg0', 'arg1'), trigger=Trigger.interval(seconds=3), job_id='test_scheduler')
    # scheduler = SchedulerManager()
    # scheduler.add_persistence_job(
    #     JOBINFO(name='test2',
    #             description='这是任务1',
    #             task='test_scheduler',
    #             trigger=Trigger.interval(seconds=3),
    #             is_active=1,
    #             args=('arg0', 'arg1'),
    #             kwargs={}))

    # SchedulerManager()
    id = test_job('arg0', 'arg1')
    import time
    while 1:
        # 十秒后停止任务，二十秒后恢复任务，三十秒后删除任务
        time.sleep(10)
        SchedulerManager.pause_job(id)
        time.sleep(10)
        SchedulerManager.resume_job(id)
        time.sleep(10)
        SchedulerManager.remove_job(id)

        # time.sleep(10)
        # # scheduler.get_persistence_jobinfo('test2').is_active = 0
        # # scheduler.refresh_presistence_job()
        # SchedulerManager.pause_job('test1')
        pass
